﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Abstract base class for notation symbols that have a duration (tones, rests).
    /// </summary>
    public abstract class DurationalSymbolElement : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="DurationalSymbolElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this symbol.
        /// </para>
        /// </param>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the voice that this symbol belongs to.
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the symbol.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> or <paramref name="voice"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The value of <paramref name="voice"/> or <paramref name="duration"/> is not valid.
        /// Please, refer to the documentation of the respective properties for details.
        /// </exception>
        protected DurationalSymbolElement(CompositionPart packagePart, ModelElement owner, VoiceDescriptorElement voice, ushort duration)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (voice == null)
                throw new ArgumentNullException("voice");

            voiceAttribute.Value = voice.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            this.voice = voice;

            ValidateAndThrow("Duration", duration);
            durationAttribute.Value = duration.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            
            customData = new ElementCollection<CustomSymbolData>(this, customDataElement);
            connectors = new ElementReferenceCollection<ConnectorElement>(this, connectorsElement);

            ++voice.SymbolsCount;
            if (voice.ShortestDuration == 0 || duration < voice.ShortestDuration)
                voice.ShortestDuration = duration;
        }

        /// <summary>
        /// Creates a new <see cref="DurationalSymbolElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this symbol.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        protected DurationalSymbolElement(CompositionPart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            durationAttribute = element.Attributes[PackageVocabulary.DurationAttribute];
            voiceAttribute = element.Attributes[PackageVocabulary.VoiceAttribute];

            byte voiceId = byte.Parse(voiceAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            voice = FindOwner<PartElement>().Voices.First(v => v.Number == voiceId);

            customDataElement = element[PackageVocabulary.CustomSymbolDataElement, PackageVocabulary.PackageNamespaceUri];
            customData = new ElementCollection<CustomSymbolData>(this, customDataElement);

            connectorsElement = element[PackageVocabulary.ConnectorElementName + "s", PackageVocabulary.PackageNamespaceUri];
            connectors = new ElementReferenceCollection<ConnectorElement>(this, connectorsElement);

            ++voice.SymbolsCount;
            if (voice.ShortestDuration == 0 || Duration < voice.ShortestDuration)
                voice.ShortestDuration = Duration;
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = CreateObjectElementCore(document);

            customDataElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.CustomSymbolDataElement,
                PackageVocabulary.PackageNamespaceUri
                );

            connectorsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ConnectorElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            durationAttribute = document.CreateAttribute(PackageVocabulary.DurationAttribute);
            voiceAttribute = document.CreateAttribute(PackageVocabulary.VoiceAttribute);

            element.Attributes.Append(durationAttribute);
            element.Attributes.Append(voiceAttribute);

            element.AppendChild(customDataElement);
            element.AppendChild(connectorsElement);

            return element;
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// When overriden in a derived class, this method creates the XML element that will
        /// hold the data of the class instance. The common durational symbol part is added
        /// by the base class.
        /// </summary>
        /// <param name="document">
        /// <para>
        /// Type: <see cref="XmlDocument"/>
        /// </para>
        /// <para>
        /// The XML document that is used to create elements and attributes.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The created element.
        /// </para>
        /// </returns>
        protected abstract XmlElement CreateObjectElementCore(XmlDocument document);

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns the first element of the <see cref="CustomData"/> collection that has a given type.
        /// </summary>
        /// <typeparam name="T">
        /// Type of the searched custom data element.
        /// </typeparam>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// Reference to the first custom symbol data extension that has the type <typeparamref name="T"/>
        /// or <see langword="null"/> if no such exists.
        /// </para>
        /// </returns>
        public T FindCustomData<T>() where T : CustomSymbolData
        {
            return (T)customData.FirstOrDefault(cd => cd is T);
        }

        #endregion

        #region Public Constants

        /// <summary>
        /// The duration of a double whole note (sometimes called breve) or rest.
        /// </summary>
        /// <value>
        /// The double whole note lasts twice as long time as the <see cref="Whole"/> note (i.e. 8 beats).<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 256.
        /// </value>
        public const ushort DoubleWhole = 256;

        /// <summary>
        /// The duration of a whole note (sometimes called semi-breve) or rest.
        /// </summary>
        /// <value>
        /// The whole note lasts 4 beats or the whole measure (regardless of its meter).<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 128.
        /// </value>
        public const ushort Whole = 128;
        
        /// <summary>
        /// The duration of a half note or rest.
        /// </summary>
        /// <value>
        /// The half note lasts 2 beats.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 64.
        /// </value>
        public const ushort Half = 64; 
        
        /// <summary>
        /// The duration of a quarter note or rest.
        /// </summary>
        /// <value>
        /// The quarter note lasts 1 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 32.
        /// </value>
        public const ushort Quarter = 32; 
        
        /// <summary>
        /// The duration of an eighth note or rest.
        /// </summary>
        /// <value>
        /// The eighth note lasts 1/2 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 16.
        /// </value>
        public const ushort Eighth = 16; 
        
        /// <summary>
        /// The duration of a sixteenth note or rest.
        /// </summary>
        /// <value>
        /// The 16-th note lasts 1/4 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 8.
        /// </value>
        public const ushort Sixteenth = 8; 
        
        /// <summary>
        /// The duration of a 32-nd note or rest.
        /// </summary>
        /// <value>
        /// The 32-nd note lasts 1/8 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 4.
        /// </value>
        public const ushort ThirtySecond = 4;
        
        /// <summary>
        /// The duration of a 64-th note or rest.
        /// </summary>
        /// <value>
        /// The 64-th note lasts 1/16 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 2.
        /// </value>
        public const ushort SixtyFourth = 2; 
        
        /// <summary>
        /// The duration of a 128-th note or rest.
        /// </summary>
        /// <value>
        /// The 128-th note lasts 1/32 beat.<br/>
        /// The duration is however expressed as the number of 128-th beats where this note corresponds to 1.
        /// </value>
        public const ushort HundredTwentyEighth = 1; 
        
        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of connectors attached to this durational symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementReferenceCollection{T}"/> of <see cref="ConnectorElement"/>
        /// </para>
        /// <para>
        /// The collection of connectors attached to this durational symbol.
        /// </para>
        /// </value>
        public ElementReferenceCollection<ConnectorElement> Connectors
        {
            get { return connectors; }
        }

        /// <summary>
        /// Gets the collection of custom symbol data extensions.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="CustomSymbolData"/>
        /// </para>
        /// <para>
        /// The collection of custom symbol data extensions.
        /// Higher layers can use this collection to extend the definition of the <see cref="DurationalSymbolElement"/>
        /// by their own data fields. For example the Common Western Music Notation classes uses this method to store
        /// the information about the staff on which the symbol should be presented at the global level, so that
        /// any new staff that represent the same composition portion can use this information.<br/>
        /// These data fields are not interpreted in any way by the <see cref="DurationalSymbolElement"/>.
        /// </para>
        /// </value>
        public ElementCollection<CustomSymbolData> CustomData
        {
            get { return customData; }
        }

        /// <summary>
        /// Gets or sets the duration of the symbol.
        /// </summary>
        /// <remarks>
        /// To set this value you can use the constants defined
        /// on the <see cref="DurationalSymbolElement"/>.
        /// </remarks>
        /// <value>
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The value is expressed as a multiple of 128-th beats,
        /// so 1 stands for a 128-th note / rest, 128 for a whole note / rest.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The duration is 0.
        /// </exception>
        /// <seealso cref="Whole"/>
        /// <seealso cref="Quarter"/>
        /// TODO: Write the validation rules
        public ushort Duration
        {
            get { return ushort.Parse(durationAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("Duration", value);

                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

                if (voice.ShortestDuration == Duration && value > voice.ShortestDuration)
                    voice.RequeryShortestDuration();
                else if (value < voice.ShortestDuration)
                    voice.ShortestDuration = value;

                durationAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Duration");
            }
        }

        /// <summary>
        /// Gets or sets the descriptor of the voice that this symbol belongs to.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the voice that this symbol belongs to.
        /// This value must not be <see langword="null"/>.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The value is <see langword="null"/>.<br/>
        /// -or-<br/>
        /// The descriptor does not belong to the same part as this symbol.<br/>
        /// -or-<br/>
        /// Another symbol exists in the same voice that would overlap with this symbol.
        /// </exception>
        /// TODO: Write the validation rules.
        public VoiceDescriptorElement Voice
        {
            get { return voice; }
            set
            {
                ValidateAndThrow("Voice", value);

                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

                if (voice != null)
                {
                    --voice.SymbolsCount;
                    if (voice.ShortestDuration == Duration)
                        voice.RequeryShortestDuration();
                }

                voice = value;
                voiceAttribute.Value = voice.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                ++voice.SymbolsCount;
                if (Duration < voice.ShortestDuration)
                    voice.ShortestDuration = Duration;

                RaisePropertyChanged("Voice");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Connectors"/> property.
        /// </summary>
        private ElementReferenceCollection<ConnectorElement> connectors;
        /// <summary>
        /// Holds the XML element that holds the <see cref="Connectors"/> collection.
        /// </summary>
        private XmlElement connectorsElement;
        /// <summary>
        /// Holds the value of the <see cref="CustomData"/> property.
        /// </summary>
        private ElementCollection<CustomSymbolData> customData;
        /// <summary>
        /// Holds the XML element that holds the <see cref="CustomData"/> collection.
        /// </summary>
        private XmlElement customDataElement;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Duration"/> property.
        /// </summary>
        private XmlAttribute durationAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Voice"/> property.
        /// </summary>
        private VoiceDescriptorElement voice;
        /// <summary>
        /// Holds the XML attribute that holds the identifier of the associated voice descriptor.
        /// </summary>
        private XmlAttribute voiceAttribute;

        #endregion
    }

    /// <summary>
    /// Common abstract base class for custom <see cref="DurationalSymbolElement"/> data extensions.
    /// </summary>
    public abstract class CustomSymbolData : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="CustomSymbolData"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        protected CustomSymbolData(CompositionPart packagePart, DurationalSymbolElement owner)
            : base(packagePart, owner)
        {
            
        }

        /// <summary>
        /// Creates a new <see cref="CustomSymbolData"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        protected CustomSymbolData(CompositionPart packagePart, DurationalSymbolElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
        }

        #endregion
    }
}
